ソースを参照

1、测量库更新

guanxinyi 10 ヶ月 前
コミット
6d18163b68

+ 337 - 0
lib/view/measure/measure_config/widgets/code_field.dart

@@ -0,0 +1,337 @@
+import 'dart:async';
+
+import 'package:fis_i18n/i18n.dart';
+import 'package:fis_measure/define.dart';
+import 'package:fis_measure/view/measure/measure_config/widgets/element_static.dart';
+import 'package:fis_measure/view/measure/measure_config/widgets/text_field.dart';
+import 'package:fis_ui/index.dart';
+import 'package:fis_ui/interface/interactive_container.dart';
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:get/get.dart';
+import 'package:fis_theme/theme.dart';
+
+/// 验证码表单栏控制器
+class VerificationCodeFieldController extends GetxController {
+  VerificationCodeFieldController({
+    this.countDown = 60,
+  });
+
+  final int countDown;
+
+  Timer? _timer;
+  bool _hasSent = false;
+
+  final _seconds = 0.obs;
+  final _isFetching = false.obs;
+  final Rx<String> _btnText = Rx('');
+
+  /// 倒计时读秒
+  int get seconds => _seconds.value;
+  set seconds(int val) => _seconds(val);
+
+  /// 是否正在获取验证码
+  bool get isFetching => _isFetching.value;
+  set isFetching(bool val) => _isFetching(val);
+
+  /// 按钮文字
+  String get btnText => setBtnText();
+  set btnText(String val) => _btnText(val);
+
+  /// 按钮是否禁用
+  bool get isBtnDisabled => isFetching || seconds > 0;
+
+  /// 开启倒计时
+  startCountDown() {
+    _hasSent = true;
+    seconds = countDown;
+    _timer = Timer.periodic(const Duration(seconds: 1), (timer) {
+      seconds--;
+      if (seconds == 0) {
+        timer.cancel();
+      }
+    });
+  }
+
+  String setBtnText() {
+    if (seconds > 0) {
+      _btnText.value =
+          i18nBook.auth.sendVerificationCodeTimer.translate([seconds]);
+    } else {
+      _btnText.value = _hasSent
+          ? i18nBook.auth.resendVerificationCode.t
+          : i18nBook.auth.sendVerificationCode.t;
+    }
+    return _btnText.value;
+  }
+
+  ///重置
+  void reset() {
+    if (_seconds.value == 0) {
+      _timer?.cancel();
+      _hasSent = false;
+      _isFetching.value = false;
+      setBtnText();
+    }
+  }
+
+  @override
+  void onClose() {
+    _timer?.cancel();
+    _timer = null;
+    super.onClose();
+  }
+}
+
+/// 验证码表单栏
+class VerificationCodeField extends StatelessWidget
+    implements FInteractiveContainer {
+  const VerificationCodeField({
+    Key? key,
+    this.tag,
+    this.pageName = 'VerificationCodeField',
+    required this.fetchCodeFunc,
+    this.textController,
+    this.textFocusNode,
+    this.onTextChanged,
+    this.disabled = false,
+    this.inError = false,
+  }) : super(key: key);
+
+  @override
+  final String pageName;
+  final String? tag;
+  final bool disabled;
+  final Future<bool> Function() fetchCodeFunc;
+  final TextEditingController? textController;
+  final FocusNode? textFocusNode;
+  final ValueChanged<String>? onTextChanged;
+  final bool inError;
+
+  @override
+  FWidget build(BuildContext context) {
+    if (kIsMobile) {
+      return _MobileLayout(
+        businessParent: this,
+        tag: tag,
+        disabled: disabled,
+        fetchCodeFunc: fetchCodeFunc,
+        textController: textController,
+        textFocusNode: textFocusNode,
+        onTextChanged: onTextChanged,
+        inError: inError,
+      );
+    }
+    final textField = _buildTextField(context);
+    final button = _buildButton(context);
+    return FFormField(builder: (state) {
+      return FRow(
+        children: [
+          FExpanded(
+            flex: 1,
+            child: FContainer(
+              margin: const EdgeInsets.only(right: 5),
+              child: textField,
+            ),
+          ),
+          QuickFWidget(button),
+        ],
+      );
+    });
+  }
+
+  Widget _buildButton(BuildContext context) {
+    final controller = Get.find<VerificationCodeFieldController>(tag: tag);
+    const textStyle = TextStyle(
+      fontSize: 14,
+      color: Colors.white,
+      height: 1,
+    );
+    final btnStyle = ElevatedButton.styleFrom(
+      elevation: 0,
+      padding: const EdgeInsets.symmetric(vertical: 18, horizontal: 16),
+      minimumSize: const Size(120, 56),
+      shape: RoundedRectangleBorder(
+        borderRadius: BorderRadius.circular(8),
+      ),
+    );
+    onBtnTap() {
+      controller.isFetching = true;
+      fetchCodeFunc().then((value) {
+        controller.isFetching = false;
+        if (value) {
+          controller.startCountDown();
+        }
+      });
+    }
+
+    return Obx(() {
+      final isDisabled = disabled || controller.isBtnDisabled;
+      final text = controller.btnText;
+      return FElevatedButton(
+        businessParent: this,
+        onPressed: isDisabled ? null : onBtnTap,
+        name: text,
+        child: FText(
+          text,
+          style: textStyle,
+        ),
+        style: btnStyle,
+      );
+    });
+  }
+
+  FWidget _buildTextField(BuildContext context) {
+    final border = FBorderTextFormField.createDesktopBorder(inError);
+
+    final textField = FTextField(
+      controller: textController,
+      focusNode: textFocusNode,
+      maxLength: 4,
+      style: const TextStyle(fontSize: 18),
+      keyboardType: TextInputType.number,
+      inputFormatters: [PageInputFormatters.number],
+      decoration: InputDecoration(
+        hintText: i18nBook.auth.hint4VerificationCode.t,
+        fillColor: TEXT_FIELD_FILL_COLOR,
+        filled: true,
+        hintStyle: const TextStyle(
+          fontSize: 16,
+          color: Colors.black54,
+        ),
+        alignLabelWithHint: true,
+        counterText: '',
+        isDense: true,
+        contentPadding: FBorderTextFormField.createDesktopContentPadding(),
+        enabledBorder: border,
+        focusedBorder: border,
+      ),
+      onChanged: onTextChanged,
+    );
+    return textField;
+  }
+}
+
+/// 验证码表单栏
+class _MobileLayout extends StatelessWidget implements FWidget {
+  const _MobileLayout({
+    this.tag,
+    required this.fetchCodeFunc,
+    this.textController,
+    this.textFocusNode,
+    this.onTextChanged,
+    this.disabled = false,
+    this.inError = false,
+    required this.businessParent,
+  });
+
+  ///父级节点
+  final FInteractiveContainer businessParent;
+  final String? tag;
+  final bool disabled;
+  final Future<bool> Function() fetchCodeFunc;
+  final TextEditingController? textController;
+  final FocusNode? textFocusNode;
+  final ValueChanged<String>? onTextChanged;
+  final bool inError;
+
+  @override
+  FWidget build(BuildContext context) {
+    final textField = _buildTextField(context);
+    final button = _buildButton(context);
+    return FFormField(builder: (state) {
+      return FRow(
+        children: [
+          FExpanded(
+            flex: 1,
+            child: FContainer(
+              margin: const EdgeInsets.only(right: 5),
+              child: textField,
+            ),
+          ),
+          if (kIsWeb)
+            button
+          else
+            FContainer(
+              height: 32,
+              child: button,
+            ),
+        ],
+      );
+    });
+  }
+
+  FWidget _buildButton(BuildContext context) {
+    final controller = Get.find<VerificationCodeFieldController>(tag: tag);
+    const textStyle = TextStyle(
+      fontSize: 14,
+      color: Colors.white,
+    );
+    final btnStyle = ElevatedButton.styleFrom(
+      elevation: 0,
+      shape: RoundedRectangleBorder(
+        borderRadius: BorderRadius.circular(4),
+      ),
+      minimumSize: const Size.fromWidth(100),
+    );
+    onBtnTap() {
+      controller.isFetching = true;
+      fetchCodeFunc().then((value) {
+        controller.isFetching = false;
+        if (value) {
+          controller.startCountDown();
+        }
+      });
+    }
+
+    return FObx(() {
+      final isDisabled = disabled || controller.isBtnDisabled;
+      final text = controller.btnText;
+      return FElevatedButton(
+        onPressed: isDisabled ? null : onBtnTap,
+        name: text,
+        businessParent: businessParent,
+        child: FText(
+          controller.btnText,
+          style: textStyle,
+        ),
+        style: btnStyle,
+      );
+    });
+  }
+
+  FWidget _buildTextField(BuildContext context) {
+    final border = OutlineInputBorder(
+      borderRadius: const BorderRadius.all(Radius.circular(4)),
+      borderSide: BorderSide(
+        color: inError
+            ? TEXT_FIELD_BORDER_ERROR_COLOR
+            : FTheme.ins.data.colorScheme.line,
+      ),
+    );
+
+    final textField = FTextField(
+      controller: textController,
+      focusNode: textFocusNode,
+      maxLength: 4,
+      keyboardType: TextInputType.number,
+      decoration: InputDecoration(
+        hintText: i18nBook.auth.hint4VerificationCode.t,
+        fillColor: TEXT_FIELD_FILL_COLOR,
+        filled: true,
+        hintStyle: const TextStyle(
+          fontSize: 12,
+          color: Colors.black54,
+        ),
+        alignLabelWithHint: true,
+        counterText: '',
+        isDense: true,
+        contentPadding: const EdgeInsets.symmetric(vertical: 8, horizontal: 26),
+        enabledBorder: border,
+        focusedBorder: border,
+      ),
+      onChanged: onTextChanged,
+    );
+    return textField;
+  }
+}

+ 23 - 0
lib/view/measure/measure_config/widgets/element_static.dart

@@ -0,0 +1,23 @@
+import 'package:flutter/services.dart';
+
+///页面输入格式限制
+class PageInputFormatters {
+  /// 禁止空格
+  static final denyWhiteSpace =
+      FilteringTextInputFormatter.deny(new RegExp(r'\b\s|\s\b'));
+
+  /// 多样性账号(手机号/邮箱/用户名)
+  static final multipleAccount = FilteringTextInputFormatter.allow(
+      new RegExp(r'@|\.|[0-9A-Za-z\u4e00-\u9fa5]'));
+
+  /// 字母或数字
+  static final letterAndNumber =
+      FilteringTextInputFormatter.allow(new RegExp(r'[0-9A-Za-z]'));
+
+  /// 禁止字母或数字
+  static final denyLetterAndNumber =
+      FilteringTextInputFormatter.deny(new RegExp(r'[0-9A-Za-z]'));
+
+  /// 纯数字
+  static final number = FilteringTextInputFormatter.allow(new RegExp(r'[0-9]'));
+}

+ 473 - 0
lib/view/measure/measure_config/widgets/form_validator.dart

@@ -0,0 +1,473 @@
+import 'dart:math' as math;
+import 'package:fis_theme/theme.dart';
+import 'package:fis_ui/index.dart';
+import 'package:flutter/material.dart';
+
+typedef FFormItemValidateStepFn = bool Function();
+
+/// 表单项验证步骤
+class FFormItemValidateStep {
+  FFormItemValidateStep({
+    required this.validateFn,
+    required this.tips,
+  });
+  FFormItemValidateStepFn validateFn;
+  String tips;
+}
+
+/// FFormItemValidator控制器
+class FFormItemValidatorController extends ChangeNotifier {
+  bool _isDisposed = false;
+  bool _isShowing = false;
+  bool _isValidating = false;
+
+  /// 是否在显示
+  bool get isShowing => _isShowing;
+
+  /// 是否在验证中
+  bool get isValidating => _isValidating;
+
+  /// 需要显示自定义错误提示
+  bool get needShowCustom => customErrorText.isNotEmpty;
+
+  /// 自定义错误提示
+  String customErrorText = '';
+
+  @override
+  void addListener(VoidCallback listener) {
+    if (_isDisposed) return;
+    super.addListener(listener);
+  }
+
+  @override
+  void removeListener(VoidCallback listener) {
+    if (_isDisposed) return;
+    super.removeListener(listener);
+  }
+
+  /// 重置验证
+  void resetValidate() {
+    if (_isDisposed) return;
+    _isValidating = false;
+    _isShowing = true;
+    notifyListeners();
+  }
+
+  /// 开始验证
+  void validate() {
+    if (_isDisposed) return;
+    _isShowing = true;
+    _isValidating = true;
+    notifyListeners();
+  }
+
+  /// 隐藏
+  void hide() {
+    if (_isDisposed) return;
+    _isShowing = false;
+    _isValidating = false;
+    notifyListeners();
+  }
+
+  /// 展示手动控制的错误提示
+  void showCustomError(String text) {
+    if (_isDisposed) return;
+    customErrorText = text;
+    notifyListeners();
+  }
+
+  /// 移除手动控制的错误提示
+  void removeCustomError() {
+    if (_isDisposed) return;
+    customErrorText = '';
+    notifyListeners();
+  }
+
+  @override
+  void dispose() {
+    _isDisposed = true;
+    super.dispose();
+  }
+}
+
+/// 表单项验证组件
+class FFormItemValidator extends StatefulWidget implements FWidget {
+  const FFormItemValidator({
+    Key? key,
+    required this.child,
+    required this.controller,
+    this.validateSteps = const [],
+    this.overlayContext,
+  }) : super(key: key);
+
+  final FWidget child;
+  final FFormItemValidatorController controller;
+  final List<FFormItemValidateStep> validateSteps;
+  final BuildContext? overlayContext;
+
+  @override
+  State<StatefulWidget> createState() => _FFormItemValidatorState();
+}
+
+class _FFormItemValidatorState extends State<FFormItemValidator> {
+  OverlayEntry? _overlayEntry;
+  late RenderBox _childBox;
+  late RenderBox _parentBox;
+  late Offset _childOffset;
+  final _arrowColor = Colors.black.withOpacity(.9);
+  static const _arrowSize = 10.0;
+  static const _verticalMargin = 0.0;
+  static const _horizontalMargin = 0.0;
+  bool _isShowCustomError = false;
+
+  BuildContext get _parentContext =>
+      widget.overlayContext ?? Overlay.of(context).context;
+
+  @override
+  void initState() {
+    super.initState();
+    widget.controller.addListener(_updateView);
+    WidgetsBinding.instance.addPostFrameCallback((call) {
+      if (mounted) {
+        _childBox = context.findRenderObject() as RenderBox;
+        _parentBox = _parentContext.findRenderObject() as RenderBox;
+        _childOffset = _childBox.localToGlobal(Offset.zero);
+      }
+    });
+  }
+
+  @override
+  FWidget build(BuildContext context) {
+    return widget.child;
+  }
+
+  @override
+  void dispose() {
+    _hideView();
+    widget.controller.removeListener(_updateView);
+    super.dispose();
+  }
+
+  void _updateView() {
+    if (widget.controller.isShowing) {
+      _hideView();
+      _isShowCustomError = false;
+      _showView();
+    } else {
+      _hideView();
+      if (widget.controller.needShowCustom) {
+        _isShowCustomError = true;
+        _showView();
+      }
+    }
+  }
+
+  void _showView() {
+    if (_overlayEntry == null) {
+      if (mounted) {
+        _childBox = context.findRenderObject() as RenderBox;
+        _parentBox = _parentContext.findRenderObject() as RenderBox;
+        _childOffset = _childBox.localToGlobal(Offset.zero);
+      }
+      _buildOverlayEntry();
+      final ctx = widget.overlayContext ?? context;
+      Overlay.of(ctx).insert(_overlayEntry!);
+    }
+  }
+
+  void _hideView() {
+    if (_overlayEntry != null) {
+      _overlayEntry?.remove();
+      _overlayEntry = null;
+    }
+  }
+
+  void _buildOverlayEntry() {
+    _overlayEntry = OverlayEntry(
+      builder: (context) {
+        return IgnorePointer(
+          child: FContainer(
+            constraints: BoxConstraints(
+              maxWidth: _parentBox.size.width - 2 * _horizontalMargin,
+              maxHeight: _parentBox.size.height -
+                  2 * _verticalMargin -
+                  _childOffset.dy -
+                  _childBox.size.height,
+            ),
+            child: _buildTooltipLayout(),
+          ),
+        );
+      },
+    );
+  }
+
+  FWidget _buildTooltipConent() {
+    return FContainer(
+      padding: const EdgeInsets.all(10.0),
+      decoration: BoxDecoration(
+        borderRadius: BorderRadius.circular(8.0),
+        color: _arrowColor,
+      ),
+      width: _childBox.size.width - 2 * _horizontalMargin,
+      child: FColumn(
+        crossAxisAlignment: CrossAxisAlignment.start,
+        children: widget.validateSteps
+            .map(
+              (e) => _ValidateStepWidget(
+                controller: widget.controller,
+                data: e,
+              ),
+            )
+            .toList(),
+      ),
+    );
+  }
+
+  /// 自定义错误提示
+  FWidget _buildCustomTooltipConent() {
+    return FContainer(
+      padding: const EdgeInsets.all(10.0),
+      decoration: BoxDecoration(
+        borderRadius: BorderRadius.circular(8.0),
+        color: _arrowColor,
+      ),
+      width: _childBox.size.width - 2 * _horizontalMargin,
+      child: FColumn(
+        crossAxisAlignment: CrossAxisAlignment.start,
+        children: [
+          _CustomValidateStepWidget(text: widget.controller.customErrorText)
+        ],
+      ),
+    );
+  }
+
+  FWidget _buildTooltipLayout() {
+    final arrow = ClipPath(
+      child: FContainer(
+        width: _arrowSize,
+        height: _arrowSize,
+        color: _arrowColor,
+      ),
+      clipper: _ArrowClipper(),
+    );
+    final content = _isShowCustomError
+        ? _buildCustomTooltipConent()
+        : _buildTooltipConent();
+
+    return FCustomMultiChildLayout(
+      delegate: _LayoutDelegate(
+        anchorSize: _childBox.size,
+        anchorOffset: _childOffset,
+        verticalMargin: _verticalMargin,
+        horizontalMargin: _horizontalMargin,
+      ),
+      children: <FWidget>[
+        FLayoutId(
+          id: _LayoutId.downArrow,
+          child: Transform.rotate(
+            angle: math.pi,
+            child: arrow,
+          ),
+        ),
+        FLayoutId(
+          id: _LayoutId.content,
+          child: FColumn(
+            mainAxisSize: MainAxisSize.min,
+            children: <FWidget>[
+              FMaterial(
+                child: content,
+                color: Colors.transparent,
+              ),
+            ],
+          ),
+        ),
+      ],
+    );
+  }
+}
+
+enum _LayoutId {
+  downArrow,
+  content,
+}
+
+class _LayoutDelegate extends MultiChildLayoutDelegate {
+  _LayoutDelegate({
+    required this.anchorSize,
+    required this.anchorOffset,
+    required this.verticalMargin,
+    required this.horizontalMargin,
+  });
+
+  final Size anchorSize;
+  final Offset anchorOffset;
+  final double verticalMargin;
+  final double horizontalMargin;
+
+  @override
+  void performLayout(Size size) {
+    Size contentSize = Size.zero;
+    Size arrowSize = Size.zero;
+    Offset contentOffset = Offset.zero;
+    Offset arrowOffset = Offset.zero;
+
+    double anchorTopY = anchorOffset.dy;
+
+    if (hasChild(_LayoutId.content)) {
+      contentSize = layoutChild(
+        _LayoutId.content,
+        BoxConstraints.loose(size),
+      );
+    }
+    if (hasChild(_LayoutId.downArrow)) {
+      arrowSize = layoutChild(
+        _LayoutId.downArrow,
+        BoxConstraints.loose(size),
+      );
+    }
+
+    arrowOffset = Offset(
+      anchorOffset.dx +
+          anchorSize.width -
+          horizontalMargin * 2 -
+          arrowSize.width * 2,
+      anchorTopY - verticalMargin - arrowSize.height,
+    );
+    contentOffset = Offset(
+      anchorOffset.dx + horizontalMargin,
+      anchorTopY - verticalMargin - arrowSize.height - contentSize.height,
+    );
+
+    if (hasChild(_LayoutId.content)) {
+      positionChild(_LayoutId.content, contentOffset);
+    }
+    if (hasChild(_LayoutId.downArrow)) {
+      positionChild(
+        _LayoutId.downArrow,
+        Offset(arrowOffset.dx, arrowOffset.dy - 0.1),
+      );
+    }
+  }
+
+  @override
+  bool shouldRelayout(MultiChildLayoutDelegate oldDelegate) => false;
+}
+
+class _ArrowClipper extends CustomClipper<Path> {
+  @override
+  Path getClip(Size size) {
+    Path path = Path();
+    path.moveTo(0, size.height);
+    path.lineTo(size.width / 2, size.height / 2);
+    path.lineTo(size.width, size.height);
+    return path;
+  }
+
+  @override
+  bool shouldReclip(CustomClipper<Path> oldClipper) {
+    return true;
+  }
+}
+
+class _ValidateStepWidget extends FStatefulWidget {
+  const _ValidateStepWidget({
+    Key? key,
+    required this.controller,
+    required this.data,
+  }) : super(key: key);
+
+  final FFormItemValidatorController controller;
+  final FFormItemValidateStep data;
+
+  @override
+  FState<_ValidateStepWidget> createState() => _ValidateStepWidgetState();
+}
+
+class _ValidateStepWidgetState extends FState<_ValidateStepWidget> {
+  late final TextStyle _textStyle;
+  static const Color _initialColor = Colors.white;
+  static const Color _rightColor = Color(0xFF66BB6A);
+  static const Color _errorColor = Color(0xFFEF5350);
+
+  @override
+  void initState() {
+    _textStyle = TextStyle(
+      fontFamily: FTheme.ins.localeSetting.fontFamily,
+      fontSize: 13,
+      color: _initialColor,
+    );
+
+    super.initState();
+
+    widget.controller.addListener(_updateView);
+  }
+
+  @override
+  FWidget build(BuildContext context) {
+    if (!widget.controller.isValidating) {
+      return FText(widget.data.tips, style: _textStyle);
+    }
+    final checked = widget.data.validateFn();
+    final color = checked ? _rightColor : _errorColor;
+
+    return FRow(
+      children: [
+        FIcon(
+          checked ? Icons.check_rounded : Icons.close_rounded,
+          color: color,
+          size: 13,
+        ),
+        FExpanded(
+          child: FText(
+            widget.data.tips,
+            style: _textStyle.copyWith(
+              color: color,
+            ),
+          ),
+        ),
+      ],
+    );
+  }
+
+  void _updateView() {
+    if (widget.controller.isShowing) {
+      setState(() {});
+    }
+  }
+
+  @override
+  void dispose() {
+    widget.controller.removeListener(_updateView);
+    super.dispose();
+  }
+}
+
+class _CustomValidateStepWidget extends FStatelessWidget {
+  _CustomValidateStepWidget({
+    Key? key,
+    required this.text,
+  }) : super(key: key);
+
+  final String text;
+  final _textStyle = TextStyle(
+    fontFamily: FTheme.ins.localeSetting.fontFamily,
+    fontSize: 13,
+    color: const Color(0xFFEF5350),
+  );
+
+  @override
+  FWidget build(BuildContext context) {
+    return FRow(
+      children: [
+        const FIcon(
+          Icons.close_rounded,
+          color: Color(0xFFEF5350),
+          size: 13,
+        ),
+        FExpanded(
+          child: FText(text, style: _textStyle),
+        ),
+      ],
+    );
+  }
+}

+ 249 - 22
lib/view/measure/measure_config/widgets/measurement_tool_selection.dart

@@ -1,15 +1,24 @@
 // ignore: must_be_immutable
 
+import 'package:fis_common/extensions/index.dart';
+import 'package:fis_common/logger/logger.dart';
 import 'package:fis_i18n/i18n.dart';
 import 'package:fis_jsonrpc/rpc.dart';
 import 'package:fis_measure/process/language/measure_language.dart';
 import 'package:fis_measure/process/workspace/measure_data_controller.dart';
+import 'package:fis_measure/process/workspace/rpc_bridge.dart';
+import 'package:fis_measure/utils/prompt_box.dart';
 import 'package:fis_measure/view/loadding/loadding.dart';
 import 'package:fis_measure/view/measure/measure_config/measure_configuation_page.dart';
+import 'package:fis_measure/view/measure/measure_config/widgets/code_field.dart';
+import 'package:fis_measure/view/measure/measure_config/widgets/form_validator.dart';
+import 'package:fis_measure/view/measure/measure_config/widgets/state.dart';
+import 'package:fis_measure/view/measure/measure_config/widgets/text_field.dart';
 import 'package:fis_ui/index.dart';
 import 'package:fis_theme/theme.dart';
 import 'package:fis_ui/interface/interactive_container.dart';
 import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
 import 'package:get/get.dart';
 
 class SelectModulePage extends FStatefulWidget
@@ -31,6 +40,8 @@ class SelectModulePage extends FStatefulWidget
 }
 
 class _SelectModulePageState extends FState<SelectModulePage> {
+  final spaceBetween = const FSizedBox(height: 14);
+
   /// 数据
   final measureData = Get.find<MeasureDataController>();
 
@@ -39,6 +50,26 @@ class _SelectModulePageState extends FState<SelectModulePage> {
 
   static const Color childButtonHighlight = Color.fromRGBO(84, 144, 249, 1);
 
+  /// 数据状态
+  final state = SignUpState();
+
+  /// 表单编辑状态
+  final formEditState = SignUpFormEditState();
+
+  /// 表单验证状态
+  late final formValidateState = SignUpFormValidateState(state);
+
+  // 文本框控制器
+  final accountTextController = TextEditingController();
+  final verificationCodeTextController = TextEditingController();
+
+  // 文本框焦点
+  final accountFocusNode = FocusNode();
+  final verificationCodeFocusNode = FocusNode();
+
+  //  表单验证控制器
+  final accountValidatorController = FFormItemValidatorController();
+
   /// 新增测量项目
   ///
   /// [chooseMeasure] 选中的测量项
@@ -59,6 +90,19 @@ class _SelectModulePageState extends FState<SelectModulePage> {
     setState(() {});
   }
 
+  /// 展示账号验证器
+  void showAccountValidator() {
+    if (formValidateState.accountBeOccupied) {
+      accountValidatorController.hide();
+      return;
+    }
+    if (state.account.isEmpty) {
+      accountValidatorController.resetValidate();
+    } else {
+      accountValidatorController.validate();
+    }
+  }
+
   @override
   void initState() {
     super.initState();
@@ -114,17 +158,195 @@ class _SelectModulePageState extends FState<SelectModulePage> {
             ),
           ],
         ),
-        isExpend
-            ? const FIcon(
-                Icons.keyboard_arrow_down_sharp,
-              )
-            : const FIcon(
-                Icons.keyboard_arrow_right,
+        FRow(
+          children: [
+            /// TODO baka 等接口好了改
+            if (itemMetaGroupTitle == "VET" && false)
+              QuickFWidget(
+                ElevatedButton(
+                  onPressed: () {
+                    Get.dialog(FSimpleDialog(
+                      title: const FText(
+                        /// TODO baka 需要新增翻译
+                        "申请试用",
+                        style: TextStyle(
+                          color: Colors.white,
+                          fontSize: 18,
+                        ),
+                      ),
+                      cancelString: i18nBook.common.cancel.t,
+                      okString: i18nBook.common.confirm.t,
+                      isDefault: true,
+                      onOk: () {
+                        /// TODO baka 需要新增接口
+                      },
+                      onCancel: () {
+                        Get.back();
+                      },
+                      onClose: () {
+                        Get.back();
+                      },
+                      children: [
+                        FContainer(
+                          width: 600,
+                          height: 500,
+                          padding: const EdgeInsets.symmetric(
+                            horizontal: 50,
+                            vertical: 40,
+                          ),
+                          child: _buildPhoneAndVerify(),
+                        ),
+                      ],
+                    ));
+                  },
+
+                  /// TODO baka 新加翻译
+                  child: const Text("申请试用"),
+                ),
               ),
+            isExpend
+                ? const FIcon(
+                    Icons.keyboard_arrow_down_sharp,
+                  )
+                : const FIcon(
+                    Icons.keyboard_arrow_right,
+                  ),
+          ],
+        ),
+      ],
+    );
+  }
+
+  void _onBuildAccountFieldClick(String val) {
+    state.account = val;
+    formValidateState.accountBeOccupied = false;
+    showAccountValidator();
+    accountValidatorController.removeCustomError();
+  }
+
+  FWidget _buildPhoneAndVerify() {
+    return FColumn(
+      children: [
+        _buildAccountField(),
+        spaceBetween,
+        QuickFWidget(
+          _buildVerificationCodeField(),
+        ),
+      ],
+    );
+  }
+
+  FWidget _buildAccountField() {
+    final validateState = formValidateState;
+    final txtField = Obx(
+      () => FBorderTextFormField(
+        controller: accountTextController,
+        focusNode: accountFocusNode,
+        maxLength: 254,
+        inError: !validateState.accountChecked && formEditState.accountEdited,
+        onChanged: (val) => _onBuildAccountFieldClick(
+          val,
+        ),
+        inputFormatters: [
+          FilteringTextInputFormatter.deny(RegExp("[\u4e00-\u9fa5]")),
+          FilteringTextInputFormatter.deny(RegExp("\\s")),
+        ],
+
+        /// TODO baka 新加翻译
+        hintText: "请输入手机号",
+      ),
+    );
+
+    return FFormItemValidator(
+      key: const ValueKey("Signup.FormItem.Account"),
+      child: QuickFWidget(txtField),
+      // overlayContext: _parentContext,
+      controller: accountValidatorController,
+      validateSteps: [
+        FFormItemValidateStep(
+          validateFn: () => validateState.accountFormatCheck,
+
+          /// TODO baka 新加翻译
+          tips: "请输入正确的手机号",
+        ),
       ],
     );
   }
 
+  Widget _buildVerificationCodeField() {
+    if (!Get.isRegistered<VerificationCodeFieldController>(
+        tag: "request_trial")) {
+      Get.lazyPut(
+        () => VerificationCodeFieldController(countDown: 120),
+        tag: "request_trial",
+      );
+    }
+    return Obx(
+      () => VerificationCodeField(
+        key: const ValueKey("Signup.FormItem.VerificationCode"),
+        tag: "request_trial",
+        textFocusNode: verificationCodeFocusNode,
+        textController: verificationCodeTextController,
+        onTextChanged: (val) => state.verificationCode = val,
+        inError: !formValidateState.verificationCodeChecked &&
+            formEditState.verificationCodeEdited,
+        fetchCodeFunc: onFetchCodeClick,
+      ),
+    );
+  }
+
+  /// 表单失焦
+  void unfocusForm() {
+    accountFocusNode.unfocus();
+    verificationCodeFocusNode.unfocus();
+  }
+
+  /// 隐藏表单验证提示
+  void hideFormValidator() {
+    accountValidatorController.hide();
+  }
+
+  /// 点击获取验证码
+  Future<bool> onFetchCodeClick() async {
+    final account = state.account;
+    bool canSend = false;
+    if (account.isNotEmpty) {
+      if (i18nBook.isCurrentChinese) {
+        canSend = account.isChineseMobileNumber;
+      }
+    }
+    if (!canSend) {
+      formEditState.accountEdited = true;
+      PromptBox.toast("请输入正确的手机号");
+      return false;
+    }
+
+    hideFormValidator();
+    unfocusForm();
+
+    // setBusy(i18nBook.auth.sendingVerificationCode.t);
+    final isMobileAccount = account.isChineseMobileNumber;
+    bool valid = false;
+    try {
+      if (isMobileAccount) {
+        valid = await RPCBridge.ins.rpc.login.sendSMSVerificationCodeAsync(
+          SendSMSVerificationCodeRequest(userPhone: account),
+        );
+      }
+      if (valid) {
+        PromptBox.toast(i18nBook.auth.toast4VerifyCodeSendSuccess.t);
+        return true;
+      }
+    } catch (e) {
+      logger.e(
+          "User registration:Send${isMobileAccount ? 'short message' : 'email'} verification code exception",
+          e);
+    }
+    PromptBox.toast(i18nBook.auth.toast4VerifyCodeSendFail.t);
+    // busy = false;
+    return false;
+  }
+
   /// 选中的测量项
   FWidget _buildSlected(ItemMetaDTO e) {
     return FGestureDetector(
@@ -201,24 +423,29 @@ class _SelectModulePageState extends FState<SelectModulePage> {
     }
     if (name.isNotEmpty) {
       return QuickFWidget(
-        RawChip(
-            backgroundColor: textColor.withOpacity(0.8),
-            label: FText(
-              name,
-              style: const TextStyle(
-                color: Colors.white,
-                fontSize: 10,
+        Tooltip(
+          message: buyStatus == WorkingItemStatusEnum.Unpaid
+              ? "如需继续使用该测量项,请联系Vinno申请试用或购买。"
+              : "",
+          child: RawChip(
+              backgroundColor: textColor.withOpacity(0.8),
+              label: FText(
+                name,
+                style: const TextStyle(
+                  color: Colors.white,
+                  fontSize: 10,
+                ),
               ),
-            ),
-            padding: const EdgeInsets.symmetric(horizontal: 0, vertical: 0),
-            shape: OutlinedBorder.lerp(
-              const RoundedRectangleBorder(),
-              const RoundedRectangleBorder(),
-              0,
-            )
+              padding: const EdgeInsets.symmetric(horizontal: 0, vertical: 0),
+              shape: OutlinedBorder.lerp(
+                const RoundedRectangleBorder(),
+                const RoundedRectangleBorder(),
+                0,
+              )
 
-            // padding: EdgeInsets.only(left: 10, right: 10, top: 5),
-            ),
+              // padding: EdgeInsets.only(left: 10, right: 10, top: 5),
+              ),
+        ),
       );
     }
     return const FSizedBox();

+ 182 - 0
lib/view/measure/measure_config/widgets/state.dart

@@ -0,0 +1,182 @@
+import 'package:get/get.dart';
+
+///登出界面状态
+class SignUpState {
+  final Rx<bool> _onSubmit = Rx(false);
+  final Rx<bool> _isShowCodeRow = Rx(true);
+  final Rx<bool> _isCodeSend = Rx(false);
+  final Rx<bool> _isAgreeClause = Rx(false);
+  final Rx<String> _account = Rx('');
+  final Rx<String> _password = Rx('');
+  final Rx<String> _reenterPassword = Rx('');
+  final Rx<String> _verificationCode = Rx('');
+  final Rx<int> _codeWaitSecond = Rx(0);
+
+  /// 账号(手机/邮箱/用户名)
+  String get account => _account.value;
+  set account(String val) => _account(val);
+
+  /// 密码
+  String get password => _password.value;
+  set password(String val) => _password(val);
+
+  /// 确认密码
+  String get reenterPassword => _reenterPassword.value;
+  set reenterPassword(String val) => _reenterPassword(val);
+
+  /// 验证码
+  String get verificationCode => _verificationCode.value;
+  set verificationCode(String val) => _verificationCode(val);
+
+  /// 是否显示验证码行
+  bool get isShowCodeRow => _isShowCodeRow.value;
+  set isShowCodeRow(bool val) => _isShowCodeRow(val);
+
+  /// 验证码是否已发送
+  bool get isCodeSend => _isCodeSend.value;
+  set isCodeSend(bool val) => _isCodeSend(val);
+
+  /// 验证码等待秒数
+  int get codeWaitSecond => _codeWaitSecond.value;
+  set codeWaitSecond(int val) => _codeWaitSecond(val);
+
+  /// 是否同意条款和政策
+  bool get isAgreeClause => _isAgreeClause.value;
+  set isAgreeClause(bool val) => _isAgreeClause(val);
+
+  /// 提交中
+  bool get onSubmit => _onSubmit.value;
+  set onSubmit(bool val) => _onSubmit(val);
+
+  /// 重置状态
+  reset() {
+    account = '';
+    password = '';
+    reenterPassword = '';
+    verificationCode = '';
+    isShowCodeRow = true;
+  }
+}
+
+class SignUpFormValidateState {
+  SignUpFormValidateState(this.data);
+  final SignUpState data;
+
+  final Rx<bool> _asyncValidating = Rx(false);
+  final Rx<bool> _accountBeOccupied = Rx(false);
+
+  /// 正在异步验证
+  bool get asyncValidating => _asyncValidating.value;
+  set asyncValidating(bool val) => _asyncValidating.value = val;
+
+  /// 账号被占用
+  bool get accountBeOccupied => _accountBeOccupied.value;
+  set accountBeOccupied(bool val) => _accountBeOccupied.value = val;
+
+  /// 账号不为空
+  bool get accountIsNotEmpty => data.account.isNotEmpty;
+
+  /// 账号格式正确
+  bool get accountFormatCheck {
+    final account = data.account;
+    // 判断是否为邮箱
+    final RegExp emailRegExp = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$');
+    if (emailRegExp.hasMatch(account)) {
+      return true;
+    }
+    if (true) {
+      // 判断是否为手机号
+      final RegExp phoneRegExp = RegExp(r'^1[0-9]{10}$');
+      if (phoneRegExp.hasMatch(account)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  /// 账号表单验证通过
+  bool get accountChecked =>
+      accountIsNotEmpty && accountFormatCheck && !accountBeOccupied;
+
+  /// 密码不为空
+  bool get pwdIsNotEmpty => data.password.isNotEmpty;
+
+  /// 密码不包含空格
+  bool get pwdNotContainBlank => !data.password.contains(' ');
+
+  /// 密码长度正确
+  bool get pwdLengthChecked =>
+      data.password.length >= 6 && data.password.length <= 20;
+
+  /// 密码字符组合正确
+  bool get pwdCharCombinChecked {
+    final pwd = data.password;
+    int pwdMatchNum = 0;
+    // 验证包含符号 - 非字母数字
+    if (RegExp(r'[^\da-zA-Z\s]').hasMatch(pwd)) pwdMatchNum++;
+    // 验证包含字母
+    if (RegExp(r'[a-zA-Z]').hasMatch(pwd)) pwdMatchNum++;
+    // 验证包含数字
+    if (RegExp(r'\d').hasMatch(pwd)) pwdMatchNum++;
+    return pwdMatchNum > 1;
+  }
+
+  /// 密码表单验证通过
+  bool get pwdChecked =>
+      pwdIsNotEmpty &&
+      pwdNotContainBlank &&
+      pwdLengthChecked &&
+      pwdCharCombinChecked;
+
+  /// 确认密码不为空
+  bool get reenterPwdIsNotEmpty => data.reenterPassword.isNotEmpty;
+
+  /// 两次密码输入一致
+  bool get reenterPwdEqualsPwd => data.password == data.reenterPassword;
+
+  /// 确认密码表单验证通过
+  bool get reenterPwdChecked => reenterPwdIsNotEmpty && reenterPwdEqualsPwd;
+
+  /// 验证码验证通过
+  bool get verificationCodeChecked => data.verificationCode.isNotEmpty;
+
+  /// 是否可提交
+  bool get isCanSubmit {
+    if (asyncValidating) return false;
+    if (data.isShowCodeRow && data.verificationCode.isEmpty) {
+      return false;
+    }
+    return accountChecked && pwdChecked && reenterPwdChecked;
+  }
+}
+
+class SignUpFormEditState {
+  final Rx<bool> _accountEdited = Rx(false);
+  final Rx<bool> _pwdEdited = Rx(false);
+  final Rx<bool> _reenterPwdEdited = Rx(false);
+  final Rx<bool> _verificationCodeEdited = Rx(false);
+
+  /// 账号已编辑
+  bool get accountEdited => _accountEdited.value;
+  set accountEdited(bool val) => _accountEdited.value = val;
+
+  /// 密码已编辑
+  bool get pwdEdited => _pwdEdited.value;
+  set pwdEdited(bool val) => _pwdEdited.value = val;
+
+  /// 确认密码已编辑
+  bool get reenterPwdEdited => _reenterPwdEdited.value;
+  set reenterPwdEdited(bool val) => _reenterPwdEdited.value = val;
+
+  /// 验证码已编辑
+  bool get verificationCodeEdited => _verificationCodeEdited.value;
+  set verificationCodeEdited(bool val) => _verificationCodeEdited.value = val;
+
+  /// 清除状态
+  void clear() {
+    accountEdited = false;
+    pwdEdited = false;
+    reenterPwdEdited = false;
+    verificationCodeEdited = false;
+  }
+}

+ 161 - 0
lib/view/measure/measure_config/widgets/text_field.dart

@@ -0,0 +1,161 @@
+import 'package:fis_theme/theme.dart';
+import 'package:fis_ui/index.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+
+const TEXT_FIELD_FILL_COLOR = Color.fromARGB(255, 251, 249, 249);
+const TEXT_FIELD_BORDER_ERROR_COLOR = Color(0xFFEF5350);
+
+class FBorderTextFormField extends StatefulWidget implements FWidget {
+  const FBorderTextFormField({
+    Key? key,
+    this.hintText,
+    this.initialValue,
+    this.focusNode,
+    this.controller,
+    this.onChanged,
+    this.maxLength,
+    this.obscureText = false,
+    this.inputFormatters,
+    this.inError = false,
+    this.isShowVisibilityIcon = false,
+  }) : super(key: key);
+  final String? hintText;
+  final String? initialValue;
+  final FocusNode? focusNode;
+  final TextEditingController? controller;
+  final ValueChanged<String>? onChanged;
+  final int? maxLength;
+  final bool obscureText;
+  final List<TextInputFormatter>? inputFormatters;
+  final bool inError;
+  final bool isShowVisibilityIcon;
+
+  @override
+  State<StatefulWidget> createState() {
+    return _FBorderTextFormFieldState();
+  }
+
+  static InputBorder createDesktopBorder(bool inError) {
+    final border = OutlineInputBorder(
+      borderRadius: const BorderRadius.all(Radius.circular(8)),
+      borderSide: BorderSide(
+        color: inError
+            ? TEXT_FIELD_BORDER_ERROR_COLOR
+            : FTheme.ins.data.colorScheme.line,
+      ),
+    );
+    return border;
+  }
+
+  static EdgeInsets createDesktopContentPadding() {
+    return const EdgeInsets.symmetric(vertical: 16, horizontal: 34);
+  }
+}
+
+class _FBorderTextFormFieldState extends State<FBorderTextFormField> {
+  bool _obscureText = false;
+
+  @override
+  void initState() {
+    _obscureText = widget.obscureText;
+    if (mounted) {
+      setState(() {});
+    }
+    super.initState();
+  }
+
+  @override
+  FWidget build(BuildContext context) {
+    if (kIsMobile) {
+      final border = OutlineInputBorder(
+        borderRadius: const BorderRadius.all(Radius.circular(4)),
+        borderSide: BorderSide(
+          color: widget.inError
+              ? TEXT_FIELD_BORDER_ERROR_COLOR
+              : FTheme.ins.data.colorScheme.line,
+        ),
+      );
+      return FTextFormField(
+        initialValue: widget.initialValue,
+        focusNode: widget.focusNode,
+        controller: widget.controller,
+        maxLength: widget.maxLength,
+        obscureText: _obscureText,
+        obscuringCharacter: '・',
+        onChanged: widget.onChanged,
+        inputFormatters: widget.inputFormatters,
+        decoration: InputDecoration(
+          fillColor: TEXT_FIELD_FILL_COLOR,
+          filled: true,
+          hintText: widget.hintText,
+          hintStyle: const TextStyle(
+            fontSize: 14,
+            color: Colors.black54,
+          ),
+          alignLabelWithHint: true,
+          counterText: '',
+          isDense: true,
+          isCollapsed: true,
+          contentPadding:
+              const EdgeInsets.symmetric(vertical: 8, horizontal: 26),
+          enabledBorder: border,
+          focusedBorder: border,
+          suffixIconConstraints: BoxConstraints.tight(const Size(48, 24)),
+          suffixIcon: widget.isShowVisibilityIcon
+              ? GestureDetector(
+                  onTap: () {
+                    setState(() {
+                      _obscureText = !_obscureText;
+                    });
+                  },
+                  child: Icon(
+                    _obscureText ? Icons.visibility_off : Icons.visibility,
+                  ),
+                )
+              : null,
+        ),
+      );
+    }
+
+    final border = FBorderTextFormField.createDesktopBorder(widget.inError);
+    return FTextFormField(
+      initialValue: widget.initialValue,
+      focusNode: widget.focusNode,
+      controller: widget.controller,
+      maxLength: widget.maxLength,
+      obscureText: _obscureText,
+      obscuringCharacter: '・',
+      onChanged: widget.onChanged,
+      inputFormatters: widget.inputFormatters,
+      style: const TextStyle(fontSize: 18),
+      decoration: InputDecoration(
+        fillColor: TEXT_FIELD_FILL_COLOR,
+        filled: true,
+        hintText: widget.hintText,
+        hintStyle: const TextStyle(
+          fontSize: 16,
+          color: Colors.black54,
+        ),
+        suffixIconConstraints: BoxConstraints.tight(const Size(48, 24)),
+        suffixIcon: widget.isShowVisibilityIcon
+            ? GestureDetector(
+                onTap: () {
+                  setState(() {
+                    _obscureText = !_obscureText;
+                  });
+                },
+                child: Icon(
+                  _obscureText ? Icons.visibility_off : Icons.visibility,
+                ),
+              )
+            : null,
+        alignLabelWithHint: true,
+        counterText: '',
+        contentPadding: FBorderTextFormField.createDesktopContentPadding(),
+        enabledBorder: border,
+        focusedBorder: border,
+      ),
+    );
+  }
+}

+ 1 - 1
lib/view/measure/measure_tool.dart

@@ -68,7 +68,7 @@ class LeftSiderSelectMeasureState extends FState<LeftSiderSelectMeasure> {
     if (selectIndex == activeItemIndex) return;
     final itemMeta = measureData.curItemMetaList[selectIndex];
     if (itemMeta.buyStatus == WorkingItemStatusEnum.Unpaid) {
-      PromptBox.toast("当前测量项需要先购买测量包");
+      PromptBox.toast("如需继续使用该测量项,请联系Vinno申请试用或购买。");
       return;
     }
     changeItem(itemMeta);